// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.util.xml.impl;

import com.intellij.openapi.Disposable;
import com.intellij.openapi.application.Application;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.extensions.ExtensionPointName;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.util.ReflectionAssignabilityCache;
import com.intellij.util.ReflectionUtil;
import com.intellij.util.containers.ConcurrentFactoryMap;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.MultiMap;
import com.intellij.util.xml.DomElement;
import com.intellij.util.xml.DomElementVisitor;
import com.intellij.util.xml.DomFileDescription;
import com.intellij.util.xml.TypeChooserManager;
import com.intellij.util.xml.highlighting.DomElementsAnnotator;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.lang.reflect.Type;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * @author peter
 */
public final class DomApplicationComponent {
  private final MultiMap<String, DomFileMetaData> myRootTagName2FileDescription = MultiMap.createSet();
  private final Set<DomFileMetaData> myAcceptingOtherRootTagNamesDescriptions = new HashSet<>();
  private final ImplementationClassCache myCachedImplementationClasses = new ImplementationClassCache(DomImplementationClassEP.EP_NAME);
  private final TypeChooserManager myTypeChooserManager = new TypeChooserManager();
  final ReflectionAssignabilityCache assignabilityCache = new ReflectionAssignabilityCache();
  private final Map<Class<?>, DomElementsAnnotator> myClass2Annotator = ConcurrentFactoryMap.createMap(key-> {
      final DomFileDescription<?> desc = findFileDescription(key);
      return desc == null ? null : desc.createAnnotator();
    }
  );
  private final Map<Class<?>, DomFileDescription<?>> myClass2Description = ConcurrentFactoryMap.createMap(this::_findFileDescription);

  private final Map<Class<?>, InvocationCache> myInvocationCaches = ConcurrentFactoryMap.create(InvocationCache::new,
                                                                                                ContainerUtil::createConcurrentSoftValueMap);
  private final Map<Class<? extends DomElementVisitor>, VisitorDescription> myVisitorDescriptions =
    ConcurrentFactoryMap.createMap(VisitorDescription::new);


  public DomApplicationComponent() {
    registerDescriptions();

    //noinspection deprecation
    addChangeListener(DomFileDescription.EP_NAME, this::extensionsChanged);
    addChangeListener(DomFileMetaData.EP_NAME, this::extensionsChanged);
    addChangeListener(DomImplementationClassEP.EP_NAME, this::extensionsChanged);
  }

  private static <T> void addChangeListener(ExtensionPointName<T> ep, Runnable onChange) {
    Application app = ApplicationManager.getApplication();
    if (app.isDisposed()) {
      return;
    }
    ep.addChangeListener(onChange, app);
  }

  private void registerDescriptions() {
    //noinspection deprecation
    for (DomFileDescription<?> description : DomFileDescription.EP_NAME.getExtensionList()) {
      registerFileDescription(description);
    }
    for (DomFileMetaData meta : DomFileMetaData.EP_NAME.getExtensionList()) {
      registerFileDescription(meta);
    }
  }

  private synchronized void extensionsChanged() {
    myRootTagName2FileDescription.clear();

    myAcceptingOtherRootTagNamesDescriptions.clear();
    myClass2Annotator.clear();
    myClass2Description.clear();

    myCachedImplementationClasses.clearCache();
    myTypeChooserManager.clearCache();

    myInvocationCaches.clear();
    assignabilityCache.clear();

    myVisitorDescriptions.clear();

    registerDescriptions();
  }

  public static DomApplicationComponent getInstance() {
    return ApplicationManager.getApplication().getService(DomApplicationComponent.class);
  }

  public synchronized int getCumulativeVersion(boolean forStubs) {
    return allMetas().mapToInt(meta -> {
      if (forStubs) {
        if (meta.stubVersion != null) {
          return meta.stubVersion + StringUtil.notNullize(meta.rootTagName).hashCode(); // so that a plugin enabling/disabling could trigger the reindexing
        }
      }
      else {
        return meta.domVersion + StringUtil.notNullize(meta.rootTagName).hashCode(); // so that a plugin enabling/disabling could trigger the reindexing
      }
      return 0;
    }).sum();
  }

  private Stream<DomFileMetaData> allMetas() {
    return Stream.concat(myRootTagName2FileDescription.values().stream(), myAcceptingOtherRootTagNamesDescriptions.stream());
  }

  @NotNull
  public synchronized List<DomFileMetaData> getStubBuildingMetadata() {
    return allMetas().filter(m -> m.hasStubs()).collect(Collectors.toList());
  }

  @Nullable
  public synchronized DomFileMetaData findMeta(DomFileDescription<?> description) {
    return allMetas().filter(m -> m.lazyInstance == description).findFirst().orElse(null);
  }

  public synchronized Set<DomFileDescription<?>> getFileDescriptions(String rootTagName) {
    return ContainerUtil.map2Set(myRootTagName2FileDescription.get(rootTagName), DomFileMetaData::getDescription);
  }

  public synchronized Set<DomFileDescription<?>> getAcceptingOtherRootTagNameDescriptions() {
    return ContainerUtil.map2Set(myAcceptingOtherRootTagNamesDescriptions, DomFileMetaData::getDescription);
  }

  synchronized void registerFileDescription(DomFileDescription<?> description) {
    registerFileDescription(new DomFileMetaData(description));
    initDescription(description);
  }

  void registerFileDescription(@NotNull DomFileMetaData meta) {
    if (StringUtil.isEmpty(meta.rootTagName)) {
      myAcceptingOtherRootTagNamesDescriptions.add(meta);
    }
    else {
      myRootTagName2FileDescription.putValue(meta.rootTagName, meta);
    }
  }

  void initDescription(@NotNull DomFileDescription<?> description) {
    for (Map.Entry<Class<? extends DomElement>, Class<? extends DomElement>> entry : description.getImplementations().entrySet()) {
      registerImplementation(entry.getKey(), entry.getValue(), null);
    }

    myTypeChooserManager.copyFrom(description.getTypeChooserManager());
  }

  synchronized void removeDescription(DomFileDescription<?> description) {
    DomFileMetaData meta = findMeta(description);
    myRootTagName2FileDescription.get(description.getRootTagName()).remove(meta);
    myAcceptingOtherRootTagNamesDescriptions.remove(meta);
  }

  @Nullable
  public synchronized DomFileDescription<?> findFileDescription(Class<?> rootElementClass) {
    return myClass2Description.get(rootElementClass);
  }

  @Nullable
  private synchronized DomFileDescription<?> _findFileDescription(Class<?> rootElementClass) {
    return allMetas()
      .map(meta -> meta.getDescription())
      .filter(description -> description != null && description.getRootElementClass() == rootElementClass)
      .findAny()
      .orElse(null);
  }

  public DomElementsAnnotator getAnnotator(Class<?> rootElementClass) {
    return myClass2Annotator.get(rootElementClass);
  }

  @Nullable
  final Class<? extends DomElement> getImplementation(Class<?> concreteInterface) {
    //noinspection unchecked
    return myCachedImplementationClasses.get(concreteInterface);
  }

  public final void registerImplementation(Class<? extends DomElement> domElementClass, Class<? extends DomElement> implementationClass,
                                           @Nullable final Disposable parentDisposable) {
    myCachedImplementationClasses.registerImplementation(domElementClass, implementationClass, parentDisposable);
  }

  TypeChooserManager getTypeChooserManager() {
    return myTypeChooserManager;
  }

  public final StaticGenericInfo getStaticGenericInfo(final Type type) {
    return getInvocationCache(ReflectionUtil.getRawType(type)).genericInfo;
  }

  final InvocationCache getInvocationCache(Class<?> type) {
    return myInvocationCaches.get(type);
  }

  public final VisitorDescription getVisitorDescription(Class<? extends DomElementVisitor> aClass) {
    return myVisitorDescriptions.get(aClass);
  }

}
